"""
Your awesome Distance Vector router for CS 168

Based on skeleton code by:
  MurphyMc, zhangwen0411, lab352
"""

import sim.api as api
from cs168.dv import RoutePacket, \
                     Table, TableEntry, \
                     DVRouterBase, Ports, \
                     FOREVER, INFINITY

class DVRouter(DVRouterBase):

    # A route should time out after this interval
    ROUTE_TTL = 15

    # Dead entries should time out after this interval
    GARBAGE_TTL = 10

    # -----------------------------------------------
    # At most one of these should ever be on at once
    # stage 6，如果不采用split horizon，则数据包会在s2和s3之间循环（因为stage4采取的策略是相同端口相同目的地则一定更新），最终超时。
    # 实际上这种方法是被动等待路由表表项过期。
    SPLIT_HORIZON = False
    # SPLIT_HORIZON = True
    # stage 7，主动对那些需要经过的邻居发送毒性路由，这样可以避免循环和等待。A1 ping B
    # POISON_REVERSE = False
    POISON_REVERSE = True
    # -----------------------------------------------
    
    # Determines if you send poison for expired routes
    # POISON_EXPIRED = False
    POISON_EXPIRED = True

    # Determines if you send updates when a link comes up
    # SEND_ON_LINK_UP = False
    SEND_ON_LINK_UP = True

    # Determines if you send poison when a link goes down
    POISON_ON_LINK_DOWN = True
    # POISON_ON_LINK_DOWN = True

    USER_LOG = True

    def __init__(self):
        """
        Called when the instance is initialized.
        DO NOT remove any existing code from this method.
        However, feel free to add to it for memory purposes in the final stage!
        """
        assert not (self.SPLIT_HORIZON and self.POISON_REVERSE), \
                    "Split horizon and poison reverse can't both be on"
        
        self.start_timer()  # Starts signaling the timer at correct rate.

        # Contains all current ports and their latencies.
        # See the write-up for documentation.
        self.ports = Ports()
        
        # This is the table that contains all current routes
        self.table = Table()
        self.table.owner = self

        self.history = dict()
        


    def add_static_route(self, host, port):
        """
        Adds a static route to this router's table.

        Called automatically by the framework whenever a host is connected
        to this router.

        :param host: the host.
        :param port: the port that the host is attached to.
        :returns: nothing.
        """
        # `port` should have been added to `peer_tables` by `handle_link_up`
        # when the link came up.
        assert port in self.ports.get_all_ports(), "Link should be up, but is not."

        # TODO: fill this in!
        self.table[host] = TableEntry(host, port, latency = self.ports.get_latency(port), expire_time = FOREVER)
        self.send_routes(force=False)

    def handle_data_packet(self, packet, in_port):
        """
        Called when a data packet arrives at this router.

        You may want to forward the packet, drop the packet, etc. here.

        :param packet: the packet that arrived.
        :param in_port: the port from which the packet arrived.
        :return: nothing.
        """
        # TODO: fill this in!
        dst = packet.dst
        if dst in self.table:
            if self.table[dst].latency < INFINITY:
                self.send(packet, self.table[dst].port, flood=False)
                return
        if self.USER_LOG:
            api.userlog.debug(f"Drop packet: from {packet.src} to {packet.dst} in {self.table.owner}")

    def send_routes(self, force=False, single_port=None):
        """
        Send route advertisements for all routes in the table.

        :param force: if True, advertises ALL routes in the table;
                      otherwise, advertises only those routes that have
                      changed since the last advertisement.
               single_port: if not None, sends updates only to that port; to
                            be used in conjunction with handle_link_up.
        :return: nothing.
        """
        # TODO: fill this in!
        # self.expire_routes()

        # ports_list = self.ports.get_all_ports() if single_port is None else [single_port]
        # if force:
        #     send_table = self.table
        # else:
        #     send_table = Table()
        #     for dst in self.table:
        #         if dst not in self.history or self.table[dst].port != self.history[dst].port or self.table[dst].latency != self.history[dst].latency:
        #             send_table[dst] = self.table[dst]

        # # 区分POISON_REVERSE和SPLIT_HORIZON
        # for port in ports_list:
        #     for dst in send_table:
        #         if port != send_table[dst].port:
        #             self.send_route(port, dst, send_table[dst].latency)
        #         else:
        #             if self.POISON_REVERSE:
        #                 self.send_route(port, dst, INFINITY)
        #             elif self.SPLIT_HORIZON:
        #                 continue
        #             else:
        #                 self.send_route(port, dst, send_table[dst].latency)
        # self.history = Table(self.table)
        
        # 重新读题，发现需要记录的history已经说的很清楚了，第一次看文档没理解清楚，导致上面的错误做法。
        # Maintain a “history” data structure that records the latest route advertisement sent out of each port
        # for each destination host. You might want to use a tuple of the relevant properties of a route

        # 注意force的含义，如果是force则发送所有路由表项，否则只发送变化的路由表项，但是实际上是需要对比上次发送的和这次发送的，即对每个端口实际发送的route
        # 对于完全不变的表项，肯定不需要发送；但对于某些改变了的表项，实际上也可能不需要发送，因为实际发送是对于每个端口来说的，而不是只用对比路由表。
        # 还要结合 POISON_REVERSE 和 SPLIT_HORIZON 两个互斥的策略。
        new_history = dict(self.history)
        ports_list = self.ports.get_all_ports() if single_port is None else [single_port]
        for port in ports_list:
            for dst in self.table:
                latency = self.table[dst].latency
                if self.POISON_REVERSE:
                    if self.table[dst].port == port:
                        latency = INFINITY
                else:
                    if self.SPLIT_HORIZON and self.table[dst].port == port:
                        continue
                if not force and port in self.history and dst in self.history[port] and self.history[port][dst] == latency:
                    continue
                self.send_route(port, dst, latency)
                if port not in new_history:
                    new_history[port] = dict()
                new_history[port][dst] = latency
        self.history = new_history
                
 
    def expire_routes(self):
        """
        Clears out expired routes from table.
        accordingly.
        """
        # TODO: fill this in!
        for dst in list(self.table.keys()):
            if self.table[dst].expire_time <= api.current_time():
                if self.POISON_EXPIRED and self.table[dst].latency != INFINITY:
                    self.table[dst] = TableEntry(dst, self.table[dst].port, INFINITY, api.current_time() + self.ROUTE_TTL)
                else:
                    self.table.pop(dst)
                if self.USER_LOG:
                    api.userlog.debug(f"Expire route: from {self.table.owner} to {dst} latency {self.table[dst].latency} expire_time {self.table[dst].expire_time}")  

    def handle_route_advertisement(self, route_dst, route_latency, port):
        """
        Called when the router receives a route advertisement from a neighbor.

        :param route_dst: the destination of the advertised route.
        :param route_latency: latency from the neighbor to the destination.
        :param port: the port that the advertisement arrived on.
        :return: nothing.
        """
        # TODO: fill this in!
        # if self.table.owner == route_dst:
            # return
        ### 注意读清楚stage 8的三条要求
        latency = self.ports.get_latency(port) + route_latency if route_latency != INFINITY else INFINITY
        if route_dst not in self.table:
            if latency != INFINITY:
                self.table[route_dst] = TableEntry(route_dst, port, latency, self.ROUTE_TTL + api.current_time())
        else:
            if route_latency == INFINITY:
                if self.table[route_dst].port == port:
                    time = self.table[route_dst].expire_time if self.table[route_dst].latency == INFINITY else self.ROUTE_TTL + api.current_time()
                    self.table[route_dst] = TableEntry(route_dst, port, INFINITY, time)
            else:
                if latency < self.table[route_dst].latency or port == self.table[route_dst].port:
                    self.table[route_dst] = TableEntry(route_dst, port, latency, self.ROUTE_TTL + api.current_time())
        
        self.send_routes(force=False)


    def handle_link_up(self, port, latency):
        """
        Called by the framework when a link attached to this router goes up.

        :param port: the port that the link is attached to.
        :param latency: the link latency.
        :returns: nothing.
        """
        self.ports.add_port(port, latency)

        # TODO: fill in the rest!
        if self.SEND_ON_LINK_UP:
            self.send_routes(force=True, single_port=port)

    def handle_link_down(self, port):
        """
        Called by the framework when a link attached to this router does down.

        :param port: the port number used by the link.
        :returns: nothing.
        """
        self.ports.remove_port(port)

        # TODO: fill this in!
        if self.POISON_ON_LINK_DOWN:
            for dst in self.table:
                if self.table[dst].port == port:
                    self.table[dst] = TableEntry(dst, port, INFINITY, api.current_time() + self.ROUTE_TTL)
            self.send_routes(force=False)
    # Feel free to add any helper methods!
